Unit tests are very useful for checking how our app is working.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.
Design for Lean Testing
Test code should be simple, short, and abstraction free.
They should also be lean.
The less complexity our test code has, the better it is.
We just do something and check the results in our tests.
Include 3 Parts in Each Test Name
We should have 3 parts in each test name.
The first part is the module being tested.
The 2nd is the circumstance that we’re testing the code in.
The expected result is what we’re checking for in our test.
This way, we can tell what’s being tested and find out what to fix if it fails.
For example, we write:
describe('User Service', function() {
describe('Add new user', function() {
it('When no password is specified, then the product status is pending', ()=> {
const user = new UserService().add(...);
expect(user.status).to.equal('pending');
});
});
});
Structure Tests by the AAA pattern
We should structure our tests with the AAA pattern.
The 3 A’s stands for Arrange, Act, and Assert.
Arrange means the setup code is added to let us do the test.
Act is doing the test.
Assert is checking the result after doing what we have.
So if we have:
describe('User Service', function() {
describe('login', function() {
it('if the password is wrong, then we cannot log in', () => {
const user = new UserService().add({ username: 'james', password: '123456' });
const result = login('james', '123');
expect(result).to.equal(false);
});
});
});
We arrange with the first line of the test.
Act is the call for the login
function.
Assert is the expect
call.
Describe Expectations in a Product Language
We should describe tests in human-like language.
The behavior should be described so that we can understand what the test is doing.
The expect
or should
should be readable by humans.
For instance, we write:
describe('User Service', function() {
describe('Add new user', function() {
it('When no password is specified, then the product status is pending', ()=> {
const user = new UserService().add(...);
expect(user.status).to.equal('pending');
});
});
});
The strings we pass into describe
and it
explains the scenarios and tests clearly.
Test Only Public Methods
We should only test public methods so that we aren’t testing implementation.
We don’t care about the implementation of our tests.
All we care about is the results.
We should look at them when we don’t get the expected results from the tests.
This is known as behavioral testing.
We’re testing behavior and nothing else.
For example, we shouldn’t write code like:
it('should add a user to database', () => {
userManager.updateUser('james', 'password');
expect(userManager._users[0].name).toBe('james');
expect(userManager._users[0].password).toBe('password');
});
We shouldn’t test private variables which can change any time.
Avoid Mocks in Favor of Stubs and Spies
We got to stub some dependencies since we can’t do everything in our tests as we do in a real environment.
Our tests shouldn’t depend on anything outside and they should be isolated.
Therefore, we stub all the dependencies that commit side effects so that we can just test what we want to test in isolation.
We just stub any network request code.
And we watch what we want to check is called with spies.
For instance, instead of mocking a database like:
it("should delete user", async () => {
//...
const dataAccessMock = sinon.mock(DAL);
dataAccessMock
.expects("deleteUser")
.once()
.withArgs(DBConfig, user, true, false);
new UserService().delete(user);
dataAccessMock.verify();
});
We spy on the function we check it’s called by writing:
it("should delete user", async () => {
const spy = sinon.spy(Emailer.prototype, "sendEmail");
new UserService().delete(user);
expect(spy.calledOnce).to.be.true;
});
We spied on the Emailer
constructor’s sendEmail
method to check if it’s called after we called UserService
instance’s delete
method.
We avoid mock any server-side interactions with the spies approach.
Conclusion
We should test with stubs and spies.
The tests should be lean and divide our tests into 3 parts.